--[[
MIT License

Copyright (c) 2023 Anggara Yama Putra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
-- Globals
AnggaraNothing = AnggaraNothing or {}
do
  local luatype = type
  local tointeger = math.tointeger
  local tablepack = table.pack
  local tableunpack = table.unpack
  local tableconcat = table.concat
  local tableremove = table.remove
  local strbyte = string.byte
  local strgmatch = string.gmatch
  local stringpack = string.pack
  local stringunpack = string.unpack

  local sentinel
  do
    local sentinel_name = 'SENTINEL'
    local function sentinel_newindex (...)
      error( 'immutable object', 2 )
    end
    sentinel = function (name)
      local result = {}
      setmetatable(result, {
        __newindex = sentinel_newindex,
        __name = name and name or sentinel_name,
        __type = 'sentinel',
      })
      return result
    end
  end

  local function type (v)
    local rawtype = luatype( v )
    if rawtype == 'table' or rawtype == 'userdata' then
      local mt_type = getmetatable(v).__type
      if mt_type ~= nil then
        return mt_type
      end
    end
    return rawtype
  end

  local function table_defaults (target, source)
    target = target or {}
    for k,v in pairs(source) do
      if target[k] == nil then
        target[k] = v
      end
    end
    return target
  end

  local function table_idefaults (target, source, n, ignore_nil)
    target = target or {}
    n = n or #source
    for i = 1, #n do
      local v = source[i]
      if not (ignore_nil and v == nil) and target[i] == nil then
        target[i] = v
      end
    end
    return target
  end

  -- Implementation of Java's String.hashCode()
  -- https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#hashCode--
  local string_hashcode
  do
    local charpattern = '[\0-\x7F\xC2-\xF4][\x80-\xBF]*'
    local function bytes_to_codepoint (...)
      local b1, b2, b3, b4 = ...

      if b4 then
        return (
          (b1 - 0xF0) * 262144
          + (b2 - 0x80) * 4096
          + (b3 - 0x80) * 64
          + (b4 - 0x80)
        )
      elseif b3 then
        return (
          (b1 - 0xE0) * 4096
          + (b2 - 0x80) * 64
          + (b3 - 0x80)
        )
      elseif b2 then
        return (
          (b1 - 0xC0) * 64
          + (b2 - 0x80)
        )
      end
      return b1
    end

    string_hashcode = function (str)
      local n = #str
      if n == 0 then
        return 0
      end
      local result = 0
      n = 0
      local code
      for v in strgmatch(str, charpattern) do
        n = n + 1
        code = bytes_to_codepoint( strbyte(v,1,-1) )
        result = 31 * result + code
      end
      if n == 1 then
        return code
      end
      return result
    end
  end

  -- Get the memory address of `obj`.
  local function address_of (obj)
    if not obj or obj == true then
      return
    end
    local otype = luatype(obj)
    if otype == 'number' or otype == 'string' then
      return
    end
    return tonumber( tostring(obj):sub(-8), 16 )
  end

  local function object_serialize (struct, identity, values)
    local id_type = luatype( identity )
    if id_type == 'table'
    or id_type == 'function'
    or id_type == 'userdata' then
      identity = address_of( identity )
    end
    identity = tointeger( identity )
    if not identity then
      error( 'identity must be an integer' )
    end
    local formats = { '<I4' }
    for i=1, #struct do
      local args = struct[i]
      formats[#formats+1] = args[1]
    end
    return stringpack( tableconcat(formats), identity, tableunpack(values) )
  end

  local function object_deserialize (struct, bin)
    local formats = { '<I4' }
    for i=1, #struct do
      local args = struct[i]
      formats[#formats+1] = args[1]
    end
    local values = tablepack( stringunpack(tableconcat(formats), bin) )
    values[#values] = nil
    local identity = tableremove( values, 1 )
    for i=1, #values do
      local value = values[i]
      local func = struct[i][2]
      if func then
        values[i] = func( value )
      end
    end
    return identity, values
  end

  AnggaraNothing.sentinel = sentinel
  AnggaraNothing.type = type
  AnggaraNothing.table_defaults = table_defaults
  AnggaraNothing.table_idefaults = table_idefaults
  AnggaraNothing.string_hashcode = string_hashcode
  AnggaraNothing.address_of = address_of
  AnggaraNothing.object_serialize = object_serialize
  AnggaraNothing.object_deserialize = object_deserialize

  --[[ tests
  do
    local sentl = AnggaraNothing.sentinel
    local ttype = AnggaraNothing.type
    local strformat = string.format
    local strhashcode = AnggaraNothing.string_hashcode
    local addrof = AnggaraNothing.address_of

    local samples = sentl()
    assert(pcall(function()samples.eektha=nil end) == false, '[sentinel] failed for __newindex')
    assert(tostring(samples):sub(1,8)=='SENTINEL', '[sentinel] failed for name')
    assert(tostring(sentl('okboomer')):sub(1,8)=='okboomer', '[sentinel] failed for user-defined __name')

    assert(ttype(0)=='number', '[type] failed for lua_type')
    assert(ttype(samples)=='sentinel', '[type] failed for __type')

    samples = {
      {'a', 97},
      {'ab', 3105},
      {'abc', 96354},
      {'aBc', 95362},
      {'anggaranothing', 2203965706},
      {'Anggaranothing', 1201972522},
      {'anggara_nothing', 2506474737},
      {'Hello, CSO!', 3723836616},
      {'﷽', 65021},
      {'мир привет!', 1221684925},
      {'', 0},
    }
    for _,v in ipairs(samples) do
      local s, e = tableunpack(v)
      assert( strhashcode(s)%0x100000000 == e, strformat('[string_hashcode] failed for "%s"', s) )
    end

    samples = {
      {function()end, true},
      {{}, true},
      {nil, false},
      {0, false},
      {'', false},
    }
    for _,v in ipairs(samples) do
      local s, e = tableunpack(v)
      assert( (addrof(s)~=nil) == e, strformat('[address_of] failed for "%s"', s) )
    end
  end
  --]]
end
